X / Github

Delay a JavaScript function call

在Node中要实现延时一个任务执行有很多种方法:

此篇文章来分析一下这些方法有什么区别。

Node中的任务分为宏任务(MacroTask)和微任务(MicroTask),在每一个宏任务阶段中间穿插着微任务。

宏任务包含:script全部代码、setTimeout、setInterval、setImmediate、I/O等。

微任务包含:Process.nextTick、Promise等。

由于Node的Event Loop是基于libuv实现的,首先来看一下libuv这一张完整的事件循环阶段图:

执行的阶段如下:

  1. 已经到期的timer,也就是 setTimeoutsetIntervel

  2. 大多数情况下,在轮询I/O之后立即调用所有I/O回调。但在某些情况下,有些回调会推迟到下一个循环,也就是在这里执行上一次推迟的I/O回调,主要是处理一些系统调用错误,比如网络通信的错误回调。

  3. 在阻塞I/O执行前执行idle, prepare句柄。

  4. 计算轮询I/O的超时时间(注1)。

  5. 根据上一步计算的时间轮询或阻塞I/O循环,文件读写操作等所有I/O相关的回调都会被调用。

  6. 调用检查句柄回调,比如 setImmediate

  7. 调用关闭句柄回调

由于 setTimeout/setInterval 的第二个参数取值范围是:1 ~ 2^31-1,如果超过这个范围则会初始化为 1,那么就会有:

setTimeout(fn, 0) === setTimeout(fn, 1)

从上面的event loop分析可以看到 setTimeout 的回调函数在 timer 阶段执行,setImmediate 的回调函数在 check 阶段执行,event loop 的开始会先检查 timer 阶段,但是在开始之前到 timer 阶段会消耗一定时间,所以就会出现两种情况:

  1. timer 前的时间大于1ms,setTimeout的时间就满足了,久先执行setTimeout的回调。

  2. timer 前的时间小于1ms,setTimeout的时间就不满足了,久先执行 check 阶段(setImmediate)的回调函数,下一次 event loop 到 timer 阶段时再去执行setTimeout。

因此 setTimeout 0ms 和 setImmediate 的谁先谁后真的不好说(不过一般来说还是setTimeout更快)。

另外看网上的这个例子:

const fs = require('fs')
fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout')
  }, 0)

  setImmediate(() => {
    console.log('setImmediate')
  })
})

因为是在 I/O 的回调里面,那么接下来的阶段就是 check handle call,所以一定是先执行 setImmediate ,再去执行 setTimeout。

const promise = Promise.resolve()
promise.then(() => {
  console.log('promise')
});
process.nextTick(() => {
  console.log('nextTick')
});

虽然 promise 与 process.nextTick 都是注册到microTask中,但 process.nextTick 总是早于 promise.then 执行,于是输出结果就是:

nextTick
promise

那么再回到最开始的问题

const promise = Promise.resolve()
setImmediate(() => {
  console.log('setImmediate');
});
setTimeout(() => {
  console.log('setTimeout');
});
promise.then(() => {
  console.log('promise')
});
process.nextTick(() => {
  console.log('nextTick')
});

这四个方法谁先谁后呢?通过上面的分析可以得到结论:

  1. nextTick

  2. promise

  3. setTimeout

  4. setImmediate

注1: